﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Utilities.IO
{
    public enum BitOrder
    {
        LittleEndian,
        BigEndian
    }
    public enum StringType
    {
        BytePrefix,
        ShortPrefix,
        IntPrefix,
        LongPrefix,
        CString,
        None
    }

    public class DataStream : Stream
    {
        public Stream BaseStream { get; protected set; }

        public BitOrder BitOrder { get; set; }
        public Encoding Encoding { get; set; }
        public Type StringPrefixType { get; set; }
        public StringType StringType { get; set; }

        public override bool CanRead
        {
            get
            {
                return BaseStream.CanRead;
            }
        }
        public override bool CanWrite
        {
            get
            {
                return BaseStream.CanWrite;
            }
        }
        public override bool CanSeek
        {
            get
            {
                return BaseStream.CanSeek;
            }
        }

        public override long Length
        {
            get
            {
                return BaseStream.Length;
            }
        }
        public override long Position
        {
            get
            {
                return BaseStream.Position;
            }
            set
            {
                BaseStream.Position = value;
            }
        }

        public bool EndOfStream
        {
            get
            {
                return Position >= Length;
            }
        }

        protected BinaryReader reader;
        protected BinaryWriter writer;

        public DataStream(Stream baseStream) : this(baseStream, BitOrder.LittleEndian, Encoding.Default, StringType.BytePrefix)
        {
        }
        public DataStream(Stream baseStream, BitOrder bitOrder) : this(baseStream, bitOrder, Encoding.Default, StringType.BytePrefix)
        {
        }
        public DataStream(Stream baseStream, StringType stringType) : this(baseStream, BitOrder.LittleEndian, Encoding.Default, stringType)
        {
        }
        public DataStream(Stream baseStream, Encoding encoding) : this(baseStream, BitOrder.LittleEndian, encoding, StringType.BytePrefix)
        {
        }
        public DataStream(Stream baseStream, BitOrder bitOrder, Encoding encoding) : this(baseStream, bitOrder, encoding, StringType.BytePrefix)
        {
        }
        public DataStream(Stream baseStream, Encoding encoding, StringType stringType) : this(baseStream, BitOrder.LittleEndian, encoding, stringType)
        {
        }
        public DataStream(Stream baseStream, BitOrder bitOrder, StringType stringType) : this(baseStream, bitOrder, Encoding.Default, stringType)
        {
        }
        public DataStream(Stream baseStream, BitOrder bitOrder, Encoding encoding, StringType stringType)
        {
            BaseStream = baseStream;
            BitOrder = bitOrder;
            Encoding = encoding;
            StringType = stringType;

            if (baseStream.CanRead)
                reader = new BinaryReader(baseStream);
            if (baseStream.CanWrite)
                writer = new BinaryWriter(baseStream);
        }

        #region Fonctions de lecture

        public virtual bool ReadBool()
        {
            return reader.ReadByte() != 0;
        }
        public virtual sbyte ReadSByte()
        {
            return reader.ReadSByte();
        }
        public virtual byte ReadByte()
        {
            return reader.ReadByte();
        }
        public virtual byte[] ReadBytes(int count)
        {
            return reader.ReadBytes(count);
        }
        public override int Read(byte[] buffer, int offset, int count)
        {
            return BaseStream.Read(buffer, offset, count);
        }
        public virtual short ReadInt16()
        {
            if (BitOrder == BitOrder.LittleEndian)
                return reader.ReadInt16();
            else
            {
                byte[] data = ReadBytes(sizeof(short));
                Array.Reverse(data);
                return BitConverter.ToInt16(data, 0);
            }
        }
        public virtual ushort ReadUInt16()
        {
            if (BitOrder == BitOrder.LittleEndian)
                return reader.ReadUInt16();
            else
            {
                byte[] data = ReadBytes(sizeof(ushort));
                Array.Reverse(data);
                return BitConverter.ToUInt16(data, 0);
            }
        }
        public virtual int ReadInt32()
        {
            if (BitOrder == BitOrder.LittleEndian)
                return reader.ReadInt32();
            else
            {
                byte[] data = ReadBytes(sizeof(int));
                Array.Reverse(data);
                return BitConverter.ToInt32(data, 0);
            }
        }
        public virtual uint ReadUInt32()
        {
            if (BitOrder == BitOrder.LittleEndian)
                return reader.ReadUInt32();
            else
            {
                byte[] data = ReadBytes(sizeof(uint));
                Array.Reverse(data);
                return BitConverter.ToUInt32(data, 0);
            }
        }
        public virtual long ReadInt64()
        {
            if (BitOrder == BitOrder.LittleEndian)
                return reader.ReadInt64();
            else
            {
                byte[] data = ReadBytes(sizeof(long));
                Array.Reverse(data);
                return BitConverter.ToInt64(data, 0);
            }
        }
        public virtual ulong ReadUInt64()
        {
            if (BitOrder == BitOrder.LittleEndian)
                return reader.ReadUInt64();
            else
            {
                byte[] data = ReadBytes(sizeof(ulong));
                Array.Reverse(data);
                return BitConverter.ToUInt64(data, 0);
            }

        }
        public virtual float ReadSingle()
        {
            if (BitOrder == BitOrder.LittleEndian)
                return reader.ReadSingle();
            else
            {
                byte[] data = ReadBytes(sizeof(float));
                Array.Reverse(data);
                return BitConverter.ToSingle(data, 0);
            }
        }
        public virtual double ReadDouble()
        {
            if (BitOrder == BitOrder.LittleEndian)
                return reader.ReadDouble();
            else
            {
                byte[] data = ReadBytes(sizeof(double));
                Array.Reverse(data);
                return BitConverter.ToDouble(data, 0);
            }
        }
        public virtual string ReadString()
        {
            if (StringType == StringType.None)
                throw new NotSupportedException();

            if (StringType == StringType.CString)
            {
                byte b = 0;
                List<byte> bytes = new List<byte>();
                
                while ((b = ReadByte()) > 0)
                    bytes.Add(b);

                return Encoding.GetString(bytes.ToArray());
            }

            int length = 0;
            if (StringType == StringType.BytePrefix)
                length = ReadByte();
            if (StringType == StringType.ShortPrefix)
                length = ReadUInt16();
            if (StringType == StringType.IntPrefix)
                length = (int)ReadUInt32();
            if (StringType == StringType.LongPrefix)
                length = (int)ReadUInt64();

            return ReadString(length);
        }
        public virtual string ReadString(int length)
        {
            byte[] data = ReadBytes(length);
            return Encoding.GetString(data);
        }
        public virtual T Read<T>()
        {
            return (T)Read(typeof(T));
        }
        public virtual object Read(Type type)
        {
            // Enumérations
            if (type.IsEnum)
                type = Enum.GetUnderlyingType(type);

            // Types basiques
            if (type == typeof(sbyte)) return ReadSByte();
            if (type == typeof(byte)) return ReadByte();
            if (type == typeof(short)) return ReadInt16();
            if (type == typeof(ushort)) return ReadUInt16();
            if (type == typeof(int)) return ReadInt32();
            if (type == typeof(uint)) return ReadUInt32();
            if (type == typeof(long)) return ReadInt64();
            if (type == typeof(ulong)) return ReadUInt16();
            if (type == typeof(float)) return ReadSingle();
            if (type == typeof(double)) return ReadDouble();
            if (type == typeof(string)) return ReadString();

            // Tableaux
            if (type.IsArray)
            {
                type = type.GetElementType();
                int length = ReadInt32();

                Array array = Array.CreateInstance(type, length);
                for (int i = 0; i < length; i++)
                    array.SetValue(Read(type), i);

                return array;
            }

            // Structures et classes
            if (type.IsValueType || type.GetCustomAttributes(true).Count(o => o is SerializableAttribute) > 0)
            {
                IEnumerable<FieldInfo> fields = type.GetFields().Where(f => f.GetCustomAttributes(true).Count(o => o is NonSerializedAttribute) == 0);
                object obj = Activator.CreateInstance(type);
                foreach (FieldInfo field in fields)
                    field.SetValue(obj, Read(field.FieldType));

                return obj;
            }

            // Elément non supporté
            throw new NotSupportedException();
        }

        public virtual void ReadAsync<T>(Action<T> method)
        {
            buffer = new byte[SizeOf<T>()];
            this.method = method;
            useOverload = false;
            BaseStream.BeginRead(buffer, 0, SizeOf<T>(), new AsyncCallback(OnReadAsync<T>), null);
        }
        public virtual void ReadAsync<T>(Action<DataStream, T> method)
        {
            buffer = new byte[SizeOf<T>()];
            this.method = method;
            useOverload = true;
            BaseStream.BeginRead(buffer, 0, SizeOf<T>(), new AsyncCallback(OnReadAsync<T>), null);
        }
        public virtual void ReadAsync(Action method)
        {
            // FIXME : 100% cpu usage
            /*new Thread(() =>
            {
                while (client.Available == 0)
                    Thread.Sleep(0);
                method();
            }).Start();*/
        }

        private byte[] buffer;
        private object method;
        private bool useOverload;
        private void OnReadAsync<T>(IAsyncResult asyncResult)
        {
            try
            {
                if (BaseStream.EndRead(asyncResult) != SizeOf<T>())
                    return;
            }
            catch
            {
                for (int i = 0; i < buffer.Length; i++)
                    buffer[i] = 0;
            }
            finally
            {
                if (typeof(T) == typeof(string))
                {
                    int length = 0;
                    if (StringPrefixType == typeof(byte)) length = CastTo<byte>(buffer);
                    if (StringPrefixType == typeof(sbyte)) length = CastTo<sbyte>(buffer);
                    if (StringPrefixType == typeof(short)) length = CastTo<sbyte>(buffer);
                    if (StringPrefixType == typeof(ushort)) length = CastTo<sbyte>(buffer);
                    if (StringPrefixType == typeof(int)) length = CastTo<sbyte>(buffer);
                    buffer = ReadBytes(length);
                }
                if (useOverload)
                    ((Action<DataStream, T>)method)(this, CastTo<T>(buffer));
                else
                    ((Action<T>)method)(CastTo<T>(buffer));
            }
        }

        protected int SizeOf<T>()
        {
            return SizeOf(typeof(T));
        }
        protected int SizeOf(Type type)
        {
            if (type.IsEnum)
                return SizeOf(Enum.GetUnderlyingType(type));

            // Types basiques
            if (type == typeof(sbyte)) return sizeof(sbyte);
            if (type == typeof(byte)) return sizeof(byte);
            if (type == typeof(short)) return sizeof(short);
            if (type == typeof(ushort)) return sizeof(ushort);
            if (type == typeof(int)) return sizeof(int);
            if (type == typeof(uint)) return sizeof(uint);
            if (type == typeof(long)) return sizeof(long);
            if (type == typeof(ulong)) return sizeof(ulong);
            if (type == typeof(float)) return sizeof(float);
            if (type == typeof(double)) return sizeof(double);
            if (type == typeof(string)) return SizeOf(StringPrefixType);

            return 0;
        }
        protected T CastTo<T>(byte[] buffer)
        {
            return (T)CastTo(typeof(T), buffer);
        }
        protected object CastTo(Type type, byte[] buffer)
        {
            // Enumération
            if (type.IsEnum)
                return CastTo(Enum.GetUnderlyingType(type), buffer);

            // Type basique
            if (type == typeof(string))
                return Encoding.GetString(buffer);
            else
            {
                if (type == typeof(sbyte)) return (sbyte)buffer[0];
                if (type == typeof(byte)) return buffer[0];

                if (BitOrder == BitOrder.BigEndian)
                    Array.Reverse(buffer);

                if (type == typeof(short)) return BitConverter.ToInt16(buffer, 0);
                if (type == typeof(ushort)) return BitConverter.ToUInt16(buffer, 0);
                if (type == typeof(int)) return BitConverter.ToInt32(buffer, 0);
                if (type == typeof(uint)) return BitConverter.ToUInt32(buffer, 0);
                if (type == typeof(long)) return BitConverter.ToInt64(buffer, 0);
                if (type == typeof(ulong)) return BitConverter.ToUInt64(buffer, 0);
                if (type == typeof(float)) return BitConverter.ToSingle(buffer, 0);
                if (type == typeof(double)) return BitConverter.ToDouble(buffer, 0);
            }

            return null;
        }

        #endregion
        #region Fonctions d'écriture

        public virtual void Write(bool value)
        {
            writer.Write((byte)(value ? 1 : 0));
        }
        public virtual void Write(sbyte value)
        {
            writer.Write(value);
        }
        public virtual void Write(byte value)
        {
            writer.Write(value);
        }
        public virtual void Write(byte[] value)
        {
            writer.Write(value);
        }
        public override void Write(byte[] buffer, int offset, int count)
        {
            BaseStream.Write(buffer, offset, count);
        }
        public virtual void Write(short value)
        {
            if (BitOrder == BitOrder.LittleEndian)
                writer.Write(value);
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(ushort value)
        {
            if (BitOrder == BitOrder.LittleEndian)
                writer.Write(value);
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(int value)
        {
            if (BitOrder == BitOrder.LittleEndian)
                writer.Write(value);
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(uint value)
        {
            if (BitOrder == BitOrder.LittleEndian)
                writer.Write(value);
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(long value)
        {
            if (BitOrder == BitOrder.LittleEndian)
                writer.Write(value);
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(ulong value)
        {
            if (BitOrder == BitOrder.LittleEndian)
                writer.Write(value);
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(float value)
        {
            if (BitOrder == BitOrder.LittleEndian)
                writer.Write(value);
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(double value)
        {
            if (BitOrder == BitOrder.LittleEndian)
                writer.Write(value);
            else
            {
                byte[] data = BitConverter.GetBytes(value);
                Array.Reverse(data);
                writer.Write(data);
            }
        }
        public virtual void Write(string value)
        {
            if (StringType == StringType.BytePrefix)
                Write((byte)value.Length);
            if (StringType == StringType.ShortPrefix)
                Write((ushort)value.Length);
            if (StringType == StringType.IntPrefix)
                Write((uint)value.Length);
            if (StringType == StringType.LongPrefix)
                Write((ulong)value.Length);

            Write(Encoding.GetBytes(value));

            if (StringType == IO.StringType.CString)
                Write((byte)0);
        }
        public virtual void Write(string value, int length)
        {
            if (value.Length > length)
                value = value.Remove(length);
            if (value.Length < length)
                value = value.PadRight(length, '\0');
            Write(value);
        }
        public virtual void Write(Array value)
        {
            int length = value.GetLength(0);
            Write(length);

            for (int i = 0; i < length; i++)
                Write(value.GetValue(i));
        }
        public virtual void Write(object value)
        {
            Type type = value.GetType();

            // Enumérations
            if (type.IsEnum)
                type = Enum.GetUnderlyingType(type);

            // Types basiques
            if (type == typeof(sbyte)) Write((sbyte)value);
            else if (type == typeof(byte)) Write((byte)value);
            else if (type == typeof(short)) Write((short)value);
            else if (type == typeof(ushort)) Write((ushort)value);
            else if (type == typeof(int)) Write((int)value);
            else if (type == typeof(uint)) Write((uint)value);
            else if (type == typeof(long)) Write((long)value);
            else if (type == typeof(ulong)) Write((ulong)value);
            else if (type == typeof(float)) Write((float)value);
            else if (type == typeof(double)) Write((double)value);
            else if (type == typeof(string)) Write((string)value);

            // Tableaux
            else if (type.IsArray)
                Write((Array)value);

            // Structures et classes
            else if (type.IsValueType || type.GetCustomAttributes(true).Count(o => o is SerializableAttribute) > 0)
            {
                IEnumerable<FieldInfo> fields = type.GetFields().Where(f => f.GetCustomAttributes(true).Count(o => o is NonSerializedAttribute) == 0);
                foreach (FieldInfo field in fields)
                    Write(field.GetValue(value));
            }

            // Elément non supporté
            else
                throw new NotSupportedException();
        }

        #endregion

        public override void Flush()
        {
            BaseStream.Flush();
        }
        public override long Seek(long offset, SeekOrigin origin)
        {
            return BaseStream.Seek(offset, origin);
        }
        public override void SetLength(long value)
        {
            BaseStream.SetLength(value);
        }
    }
}